我們已經知道怎麼找出一張影像的特徵點,但是線的特徵也同樣重要,因此我們要從中提取有意義的線條。
邊緣偵測 (edge detection) 的目標是識別出影像中亮度(或顏色)發生劇烈變化的像素點。這些點連起來,通常就構成了物體的邊界和輪廓。它是許多高階視覺任務的基礎,例如物件分割、形狀分析和物體辨識。
Canny 邊緣偵測是一個相當良好的邊緣偵測演算法,有著三個核心標準:低錯誤率、精確的邊緣定位、對單一邊緣只有單一響應。
這個演算法能分為五個步驟。
高斯濾波:首先對影像進行高斯濾波,以平滑影像並去除無關的雜訊。
使用 Sobel 算子計算影像中每個像素點的梯度強度和方向。梯度強度大的地方,很可能就是邊緣。
非極大值抑制:Sobel 算子產生的邊緣通常很粗。此步驟的目的是將粗邊緣「細化」成單像素寬度的線條。它會檢查一個像素在它的梯度方向上,是否是其鄰域中的局部最大值。如果不是,就將該點設為0。
雙閾值檢測:使用高與低閾值
梯度強度大於高閾值時,標記為強邊緣點。
梯度強度介於兩個閾值之間,標記為弱邊緣點。
梯度強度小於低閾值時,直接捨棄。
Canny 演算法給了我們一堆零散的邊緣像素點。但如果我們想知道「這張圖裡有幾條直線?」或「最長的那條線在哪裡?」,Canny 本身無法回答。而這時就需要用到 Hough 變換。
Hough 變換是一種特徵提取技術,用於在影像中辨識特定形狀的實例,最常用於偵測直線和圓形。
其偵測直線的核心思想是:將影像空間中的一個點,變換到參數空間中的一條曲線。
在笛卡兒座標系中,一條直線是
但這種表示法無法處理垂直線(斜率 m 為無窮大)。
因此,Hough 變換使用極座標表示法
其中 ρ 是原點到直線的垂直距離,θ 是該垂直線與x軸的夾角。
此時影像空間中共線的所有點 (x, y),在 (ρ, θ) 參數空間中,它們對應的所有正弦曲線都會交於同一個點。
演算法流程如下:
對輸入影像(通常是 Canny 的輸出)中的每一個邊緣點,計算出它在 Hough 空間中對應的所有 (ρ, θ) 組合,並在一個二維的「累加器」矩陣上投票。
投票結束後,累加器中值最高的點,就對應著原始影像中最顯著的直線。
import cv2
import numpy as np
# 建議使用有清晰輪廓的圖片
image_path = 'bird.jpg'
image = cv2.imread(image_path)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 使用高斯模糊降噪
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# 使用 Canny 進行邊緣偵測
# cv2.Canny(image, threshold1, threshold2)
# threshold1: 低閾值
# threshold2: 高閾值
canny_edges = cv2.Canny(blurred, 50, 150)
cv2.imshow("Original", image)
cv2.imshow("Canny Edges", canny_edges)
cv2.imwrite("Canny.jpg", canny_edges)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 使用機率霍夫變換來偵測直線
# cv2.HoughLinesP(image, rho_accuracy, theta_accuracy, threshold, minLineLength, maxLineGap)
# threshold: 累加器閾值,得票數高於此值的線才會被回傳
# minLineLength: 線段的最短長度
# maxLineGap: 同一條線上,兩點之間可接受的最大間隙
lines = cv2.HoughLinesP(canny_edges, 1, np.pi / 180, threshold=60, minLineLength=50, maxLineGap=10)
# 建立一個副本來繪製線條
line_image = image.copy()
if lines is not None:
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(line_image, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.imshow("Hough Lines", line_image)
cv2.imwrite("Hough.jpg", line_image)
cv2.waitKey(0)
cv2.destroyAllWindows()